今天我們來一次好久沒做的實作,做出 Microsoft rand() ,若是使用 Dev-C++ 進行程式開發的人,多少會有使用過 random 函數,那有沒有人注意過這個 random 函數的亂數產生過程 ? 你可以用 debugger 去追蹤 rand 的算法,這裡我也可以提供一個別人所整理的亂數產生器列表,這個表是我之前從 DEVCORE 破解 IDA pro 的偽隨機數的 blog 中注意到的,在連結裡面的 C 的 rand 底下還有一個小標題--Microsoft rand(),就是我們這次的目標。
據這篇描述,亂數最大值 RAND_MAX 為 32767,算法為
但其實我看不懂他的參數...,因為就我自己追到的算法應該為
next_seed = seed * 0x343FD + 0x269EC3
result = next_seed >> 16 & 0x7FFF
目前的種子碼乘以 0x343FD ( 214013 ) 再加上 0x269EC3 ( 2531011 ),計算出的結果為下一輪的種子碼,再繼續將這個值向右位移 16 bit 並與 0x7FFF 做 AND 運算後即為亂數值。記得要注意到種子碼 seed 會預設為 1,所以若以初始種子碼下去做第一輪運算
0x29E2C0 = 1 * 0x343FD + 0x269EC3
0x29 = 0x29E2C0 >> 0x10 & 0x7FFF
用初始值 1 可以算出 0x29E2C0,此值作為第二輪的種子碼。
0x29E2C0 向右為移 16 bit 並與 0x7FFF 做 AND 後可得 0x29,也就是 41。
若又進行第二輪的計算
0x4823F683 = 0x29E2C0 * 0x343FD + 0x269EC3 //記得這邊只用到 31 位元
0x4823 = 0x4823F683 >> 0x10 & 0x7FFF
用前一輪得到的種子碼 0x29E2C0 下去運算得到 0x4823F683,記得這邊只有使用 31 位元下去做運算,所以原本計算出的 0x88C823F683 要取下 31 bit,也就是 0x4823F683,此值又會再作為下一輪的種子碼。
0x4823F683 向右位移 16 bit 在與 0x7FFF 做 AND 後可得 0x4823,也就是 18467。
這樣子的運算方法稱為線性同餘法,相乘相加再取餘數就是線性同餘法的標準計算方式。
亂數產生器大致介紹到這邊,接下來我先提出這次練習的目標,做出一個 Microsoft rand() 的亂處產生器,輸入產生的亂數個數,並從第一個亂數結果一直輸出到要求的個數,類似於
for(i=1;i<要求個數;i++){
printf("%d",rand());
}
大家可以自己試著先寫成 C,再慢慢將指令寫成組語,下面是我組語的設計邏輯,明天我會放上我的組語程式碼,當然中間計算過程要拆成一條指令一條指令做。如果防火牆一直跳出來警告的話可以先把放組語的資料夾設定為不檢查。
seed 為種子碼
count 為計數器
time 為要求個數
random 為產生的亂數值
可參考連結 (在撰寫的過程中應該會需要)
x86 and amd64 instruction reference => 參考指令功能與結構
NASM doc => 參考 NASM 的基本保留字或其他功能
Compiler Explorer => 參考除了函數內部以外的組語結構
我好像沒有提到一件事,不知道有沒有人好奇之前 HelloWorld 的字串後面為甚麼接了一個零,那個其實是十進位的 ASCII,若去對表會發現 0 是 NULL,而換行的話就是 10,所以如果 printf 需要格式化輸出的格式的話可以試著使用 format1 和 format2,format1 就是單純輸出數字而已,而 format2 則輸出數字後還會換行,記得不是使用 format3 喔,那只會跟著輸出反斜線跟 n 而已。
format1: db "%d",0
format2: db "%d",10,0
format3: db "%d\n",0
還有會用到 [ ] ,有點類似於取值,因為組語的標籤其實是位址,例如 push format1 ,其實就是將 format1 的位址傳入 Stack 中,因此如果要取得裡面的值的話要用 [ ] 把要取值的標籤括起來。